//-----------------------------------------------------------------------------
// AnimationPlayer.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------

using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using ShevaEngine.Core.Modules.Scenes;
using ShevaEngine.Core.Modules.Animations;

namespace ShevaEngine2.Modules.Animations
{
    /// <summary>
    /// The animation player is in charge of decoding bone position
    /// matrices from an animation clip.
    /// </summary>
    public class AnimationPlayer
    {
        // Information about the currently playing animation clip.
        private AnimationClip currentClipValue;
        private TimeSpan currentTimeValue;
        private int currentKeyframe;

        private AnimationClip previousClipValue;
        private TimeSpan previousTimeValue;
        private int previousKeyframe;
        /// <summary>Interpolation Time.</summary>
        private double InterpolationTime;
        /// <summary>Actual Interpolation Time.</summary>
        private double ActualInterpolationTime;

        // Current animation transform matrices.
        Matrix[] BoneTransforms;
        Matrix[] PreviousBoneTransforms;
        Matrix[] worldTransforms;
        /// <summary>Animation transforms.</summary>
        public Matrix[] AnimationTransforms { get; private set; }


        // Backlink to the bind pose and skeleton hierarchy data.
        SkinningData SkinningDataValue;
        /// <summary>Attached objects.</summary>
        private List<AttachToAnimationBridge>[] AttachedObjects;

        /// <summary>
        /// Constructs a new animation player.
        /// </summary>
        public AnimationPlayer(SkinningData skinningData)
        {
            if (skinningData == null)
                throw new ArgumentNullException("skinningData");

            this.SkinningDataValue = skinningData;

            this.BoneTransforms = new Matrix[skinningData.BindPose.Count];
            this.PreviousBoneTransforms = new Matrix[skinningData.BindPose.Count];
            this.worldTransforms = new Matrix[skinningData.BindPose.Count];
            this.AnimationTransforms = new Matrix[skinningData.BindPose.Count];
            this.AttachedObjects = new List<AttachToAnimationBridge>[skinningData.BindPose.Count];

            for (int i = 0; i < skinningData.BindPose.Count; i++)
                this.AttachedObjects[i] = new List<AttachToAnimationBridge>();
        }
        
        /// <summary>
        /// Starts decoding the specified animation clip.
        /// </summary>
        public void StartClip(string clipName)
        {
            if (!this.SkinningDataValue.AnimationClips.ContainsKey(clipName))
                if (this.SkinningDataValue.AnimationClips.Count > 0)
                    clipName = this.SkinningDataValue.AnimationClips.First<KeyValuePair<string, AnimationClip>>(item => true).Key;
                else
                    throw new InvalidOperationException("No animations !");

            this.previousClipValue = this.currentClipValue;
            this.previousTimeValue = this.currentTimeValue;
            this.previousKeyframe = this.currentKeyframe;

            for (int i = this.BoneTransforms.Length - 1; i >= 0; i--)
                this.PreviousBoneTransforms[i] = this.BoneTransforms[i];

            this.currentClipValue = this.SkinningDataValue.AnimationClips[clipName];
            this.currentTimeValue = TimeSpan.Zero;
            this.currentKeyframe = 0;

            this.InterpolationTime = 0.1;
            this.ActualInterpolationTime = this.InterpolationTime;

            // Initialize bone transforms to the bind pose.
            this.SkinningDataValue.BindPose.CopyTo(BoneTransforms, 0);
        }
        
        /// <summary>
        /// Advances the current animation position.
        /// </summary>
        public void Update(GameTime time)
        {
            Matrix rootTransform = Matrix.Identity;

            this.UpdateBoneTransforms(time.ElapsedGameTime);
            this.UpdateWorldTransforms(rootTransform, BoneTransforms);
            this.UpdateSkinTransforms();
        }


        /// <summary>
        /// Helper used by the Update method to refresh the BoneTransforms data.
        /// </summary>
        public void UpdateBoneTransforms(TimeSpan time)
        {
            if (currentClipValue == null)
                throw new InvalidOperationException(
                            "AnimationPlayer.Update was called before StartClip");

            // Update the animation position.
            this.currentTimeValue += time;

            // If we reached the end, loop back to the start.
            while (this.currentTimeValue.TotalSeconds >= this.currentClipValue.Duration.TotalSeconds)
            {
                this.currentTimeValue = this.currentTimeValue.Subtract(this.currentClipValue.Duration);
                this.currentKeyframe = 0;
            }

            // Read keyframe matrices.
            IList<Keyframe> currentkeyframes = currentClipValue.Keyframes;

            if (this.previousClipValue != null)
            {
                // Update the animation position.
                this.previousTimeValue += time;

                // If we reached the end, loop back to the start.
                while (this.previousTimeValue.TotalSeconds >= this.previousClipValue.Duration.TotalSeconds)
                {
                    this.previousTimeValue = this.previousTimeValue.Subtract(this.previousClipValue.Duration);
                    this.previousKeyframe = 1;
                }

                // Read keyframe matrices.
                IList<Keyframe> previousKeyframes = previousClipValue.Keyframes;                

                while (this.currentKeyframe < currentkeyframes.Count)
                {
                    Keyframe currentKeyframe = currentkeyframes[this.currentKeyframe];

                    // Stop when we've read up to the current time position.
                    if (currentKeyframe.Time > this.currentTimeValue)
                        break;

                    // Use this keyframe.
                    this.BoneTransforms[currentKeyframe.Bone] = currentKeyframe.Transform;

                    this.currentKeyframe++;
                }
                
                while (this.previousKeyframe < previousKeyframes.Count)
                {
                    Keyframe previousKeyframe = previousKeyframes[this.previousKeyframe];

                    // Stop when we've read up to the current time position.
                    if (previousKeyframe.Time > this.previousTimeValue)
                        break;

                    this.PreviousBoneTransforms[previousKeyframe.Bone] = previousKeyframe.Transform;

                    // Use this keyframe.
                    //boneTransforms[previousKeyframe.Bone] =
                    //    Matrix.Lerp(boneTransforms[previousKeyframe.Bone], previousKeyframe.Transform, 0.5f);

                    this.previousKeyframe++;
                }

                for (int i = 0; i < this.BoneTransforms.Length; i++)
                {
                    double test = (float)(this.ActualInterpolationTime / this.InterpolationTime);

                    this.BoneTransforms[i] = Matrix.Lerp(BoneTransforms[i], this.PreviousBoneTransforms[i],
                        (float)(this.ActualInterpolationTime / this.InterpolationTime));
                }

                this.ActualInterpolationTime -= time.TotalSeconds;

                if (this.ActualInterpolationTime < 0)
                    this.previousClipValue = null;
            }
            else
            {
                while (this.currentKeyframe < currentkeyframes.Count)
                {
                    Keyframe currentKeyframe = currentkeyframes[this.currentKeyframe];

                    // Stop when we've read up to the current time position.
                    if (currentKeyframe.Time > currentTimeValue)
                        break;

                    // Use this keyframe.
                    BoneTransforms[currentKeyframe.Bone] = currentKeyframe.Transform;

                    this.currentKeyframe++;
                }
            }
        }


        /// <summary>
        /// Helper used by the Update method to refresh the WorldTransforms data.
        /// </summary>
        public void UpdateWorldTransforms(Matrix rootTransform, Matrix[] boneTransforms)
        {
            // Root bone.
            worldTransforms[0] = boneTransforms[0] * rootTransform;

            // Child bones.
            for (int bone = 1; bone < worldTransforms.Length; bone++)
            {
                int parentBone = SkinningDataValue.SkeletonHierarchy[bone];

                worldTransforms[bone] = boneTransforms[bone] *
                                             worldTransforms[parentBone];

                for (int i = this.AttachedObjects[bone].Count - 1; i >= 0; i--)
                    this.AttachedObjects[bone][i].AnimationWorld = worldTransforms[bone];
            }
        }


        /// <summary>
        /// Helper used by the Update method to refresh the SkinTransforms data.
        /// </summary>
        public void UpdateSkinTransforms()
        {
            for (int bone = 0; bone < AnimationTransforms.Length; bone++)
            {
                this.AnimationTransforms[bone] =
                    this.SkinningDataValue.InverseBindPose[bone] * worldTransforms[bone];
            }
        }


        /// <summary>
        /// Gets the current bone transform matrices, relative to their parent bones.
        /// </summary>
        public Matrix[] GetBoneTransforms()
        {
            return BoneTransforms;
        }


        /// <summary>
        /// Gets the current bone transform matrices, in absolute format.
        /// </summary>
        public Matrix[] GetWorldTransforms()
        {
            return worldTransforms;
        }


        /// <summary>
        /// Gets the current bone transform matrices,
        /// relative to the skinning bind pose.
        /// </summary>
        public Matrix[] GetSkinTransforms()
        {
            return AnimationTransforms;
        }


        /// <summary>
        /// Gets the clip currently being decoded.
        /// </summary>
        public AnimationClip CurrentClip
        {
            get { return currentClipValue; }
        }


        /// <summary>
        /// Gets the current play position.
        /// </summary>
        public TimeSpan CurrentTime
        {
            get { return currentTimeValue; }
        }

        /// <summary>
        /// Method attaches scene object to bone.
        /// </summary>
        /// <param name="sceneNode"></param>
        /// <param name="boneName"></param>
        /// <returns></returns>
        public bool AttachSceneObjectToBone(AttachToAnimationBridge sceneObject, string boneName)
        {
            if (this.SkinningDataValue.BoneIndices.ContainsKey(boneName))
            {
                this.AttachedObjects[this.SkinningDataValue.BoneIndices[boneName]].Add(sceneObject);

                return true;
            }

            return false;
        }
    }
}
